﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace StyleMVVM.Suspension
{
	public sealed class SyncStrategy
	{
		private static readonly MethodInfo dictionaryTryMethod;
		private static readonly PropertyInfo dictionaryIndexProperty;
		private static readonly MethodInfo syncCompleteMethod;

		private readonly Type suspendType;
		private Action<object, IDictionary<string, object>> suspendDelegate;
		private Action<object, IDictionary<string, object>> resumeDelegate;

		static SyncStrategy()
		{
			dictionaryTryMethod =
				typeof(IDictionary<string, object>).GetTypeInfo().GetDeclaredMethod("TryGetValue");

			TypeInfo info = typeof(IDictionary<string, object>).GetTypeInfo();
			dictionaryIndexProperty =
				typeof(IDictionary<string, object>).GetTypeInfo().GetDeclaredProperty("Item");

			syncCompleteMethod = typeof(ISyncAware).GetTypeInfo().GetDeclaredMethod("SyncComplete");
		}

		public SyncStrategy(Type typeToSuspend)
		{
			suspendType = typeToSuspend;

			init();
		}

		public IDictionary<string, object> Suspend(object suspendedObject)
		{
			Dictionary<string, object> returnValue = new Dictionary<string, object>();

			if (suspendDelegate != null)
			{
				suspendDelegate(suspendedObject, returnValue);
			}

			return returnValue;
		}

		public void Resume(object resumeObject, IDictionary<string, object> resumeInfo)
		{
			if (resumeDelegate != null)
			{
				resumeDelegate(resumeObject, resumeInfo);
			}
		}

		private void init()
		{
			List<PropertyInfo> suspendProperties = new List<PropertyInfo>();

			foreach (PropertyInfo property in suspendType.GetTypeInfo().DeclaredProperties)
			{
				foreach (Attribute customAttribute in property.GetCustomAttributes(true))
				{
					if (customAttribute is ISyncAttribute)
					{
						suspendProperties.Add(property);
					}
				}
			}

			suspendDelegate = buildSuspendDelegate(suspendProperties);
			resumeDelegate = buildResumeDelegate(suspendProperties);
		}

		private Action<object, IDictionary<string, object>>
			buildSuspendDelegate(List<PropertyInfo> suspendProperties)
		{
			List<Expression> assignStatments = new List<Expression>();

			// Define method parameters
			ParameterExpression dictionaryParam =
				Expression.Parameter(typeof(IDictionary<string, object>));
			ParameterExpression suspendObject =
				Expression.Parameter(typeof(object));

			// object representing the properly typed object
			ParameterExpression castObject = Expression.Variable(suspendType);

			// static cast the suspendedObject to its proper type and assign it to
			// castObject variable
			assignStatments.Add(
				Expression.Assign(
					castObject, Expression.Convert(suspendObject, suspendType)));

			foreach (PropertyInfo property in suspendProperties)
			{
				IndexExpression dictionaryProperty =
					Expression.Property(dictionaryParam, dictionaryIndexProperty, Expression.Constant(property.Name));

				MemberExpression memberExpression = Expression.Property(castObject, property);

				assignStatments.Add(
					Expression.Assign(dictionaryProperty,
					                  Expression.Convert(memberExpression, typeof(object))));
			}

			BlockExpression block = Expression.Block(new[] { castObject }, assignStatments);

			return Expression.Lambda<Action<object, IDictionary<string, object>>>(
				block, true, new[] { suspendObject, dictionaryParam }).Compile();
		}

		private Action<object, IDictionary<string, object>>
			buildResumeDelegate(List<PropertyInfo> resumeProperties)
		{
			List<Expression> assignStatments = new List<Expression>();

			// Define method parameters
			ParameterExpression dictionaryParam =
				Expression.Parameter(typeof(IDictionary<string, object>));
			ParameterExpression resumeObject =
				Expression.Parameter(typeof(object));

			// object representing the properly typed object
			ParameterExpression castObject = Expression.Variable(suspendType, "castObject");
			ParameterExpression tempO = Expression.Variable(typeof(object), "tempO");

			// static cast the suspendedObject to its proper type and assign it to
			// castObject variable
			assignStatments.Add(
				Expression.Assign(
					castObject, Expression.Convert(resumeObject, suspendType)));

			foreach (PropertyInfo property in resumeProperties)
			{
				MethodCallExpression dictionaryTryCall =
					Expression.Call(dictionaryParam,
					                dictionaryTryMethod,
					                Expression.Constant(property.Name),
					                tempO);

				BinaryExpression assignProperty =
					Expression.Assign(Expression.Property(castObject, property),
					                  Expression.Convert(tempO, property.PropertyType));

				ConditionalExpression ifStatement =
					Expression.IfThen(dictionaryTryCall, assignProperty);

				assignStatments.Add(ifStatement);
			}

			if (suspendType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(ISyncAware)))
			{
				assignStatments.Add(Expression.Call(castObject, syncCompleteMethod));
			}

			BlockExpression block = Expression.Block(new[] { castObject, tempO }, assignStatments);

			return Expression.Lambda<Action<object, IDictionary<string, object>>>(
				block, true, new[] { resumeObject, dictionaryParam }).Compile();
		}
	}
}